//
// Created by momolove on 22-12-2.
//

#include "cfclient.h"

int main(int argc, char *argv[]) {
    parse_args(argc, argv);
    do_fclient();
}

void print_help_and_exit(const char *arg0) {
    fprintf(stdout, "Usage: %s [OPTION] ...\n"
                    "Download file from fserver over TCP\n"
                    "\n"
                    "options:\n"
                    "    -l, --list          show remote served files [default action]\n"
                    "    -s, --server        specify server address [default addr is localhost]\n"
                    "    -p, --port          specify server port [default port is 12345]\n"
                    "    -d, --dl=FILE       download specified file\n"
                    "    -o, --output=FILE   output to the FILE\n"
                    "    -h, --help          display this help and exit\n", arg0);
    exit(FCLIENT_SUCCESS);
}

void parse_args(int argc, char *argv[]) {
    BOOL force_action = 0b00;

    int opt;                // return value of getopt_long()
    int option_index = 1;   // skip the first argument which is the program name

    while ((opt = getopt_long(argc, argv, short_options_client, long_options_client, &option_index)) != -1) {
        switch (opt) {
            case 'l':
                force_action |= ACTION_LIST;
                break;
            case 's':
                if (strlen(optarg) > 0) {
                    options.server = optarg;    // points to argv[i] to main, safe to use in the program
                    break;
                } else {
                    // more likely getopt_long will catch the error (option requires an argument -- 's')
                    print_error_and_exit(ERR_FCLIENT_ARGUMENT_MISSING, "Missing server address after -s/--server");
                }
            case 'p':
                options.port = atoi(optarg);    // NOLINT: atoi is safe here
                if (options.port <= 0 || options.port > 65535) {
                    print_error_and_exit(ERR_FCLIENT_PORT_INVALID, "Invalid port number: %s", optarg);
                }
                break;
            case 'd':
                if (strlen(optarg) > 0) {
                    options.action = ACTION_DOWNLOAD;
                    options.download = optarg;
                    break;
                } else {
                    // more likely getopt_long will catch the error (option requires an argument -- 'd')
                    print_error_and_exit(ERR_FCLIENT_ARGUMENT_MISSING, "Missing filename after -d/--dl");
                }
            case 'o':
                if (strlen(optarg) > 0) {
                    options.output = optarg;
                    break;
                } else {
                    // more likely getopt_long will catch the error (option requires an argument -- 'o')
                    print_error_and_exit(ERR_FCLIENT_ARGUMENT_MISSING, "Missing filename after -o/--output");
                }
            case 'h':
                print_help_and_exit(argv[0]);
            case '?':
                print_error_and_exit(ERR_FCLIENT_ARGUMENT_INVALID, "Unknown argument %s", argv[option_index]);
            default:
                print_error_and_exit(ERR_FCLIENT_INTERNAL_ERROR, "Internal error, optarg is %s", optarg);
        }
    }

    options.action |= force_action;
}

void do_fclient() {
    if (options.action & ACTION_LIST) {
        list_file();
    }
    if (options.action & ACTION_DOWNLOAD) {
        download_file();
    }
}

void connect_to_server() {
    struct addrinfo template = { AI_ADDRCONFIG, AF_INET, SOCK_STREAM };
    struct addrinfo *server_ai_list;
    int err;
    // TODO check the format of the address
    if ((err = getaddrinfo(options.server, NULL, &template, &server_ai_list)) != 0) {
        err_cleanup();
        print_error_and_exit(ERR_FCLIENT_INTERNAL_ERROR, "Error: getaddrinfo failed - %s", gai_strerror(err));
    }
    if (!server_ai_list) {
        err_cleanup();
        print_error_and_exit(ERR_FCLIENT_ADDRESS_INVALID, "Error: no available address");
    }

    for (struct addrinfo *aip = server_ai_list; aip != NULL; aip = aip->ai_next) {
        struct sockaddr_in *addr = (struct sockaddr_in *) aip->ai_addr;
        addr->sin_port = options.port;
        if ((socket_fd = connect_to_server_impl((struct sockaddr *) addr, aip->ai_addrlen)) > 0) {
            char addr_str[INET_ADDRSTRLEN];
            print_line("Connected to server %s:%d", inet_ntop(AF_INET, &addr->sin_addr, addr_str, INET_ADDRSTRLEN), addr->sin_port);
            break;
        }
    }
    // don't forget to free the memory before return
    freeaddrinfo(server_ai_list);
    if (socket_fd <= 0) {
        err_cleanup();
        print_error_and_exit(ERR_FCLIENT_CONNECTION_FAILED, "Error: failed to connect to server %s:%d", options.server, options.port);
    }
}

int connect_to_server_impl(struct sockaddr *addr, socklen_t addr_len) {
    int fd;
    for (int current_wait_sec = 1; current_wait_sec <= MAX_RETRY; current_wait_sec <<= 1) {
        if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
            return -1;  // socket creation failed
        }
        if (connect(fd, addr, addr_len) == 0) {
            return fd;
        }
        // connection failed, sleep and retry
        close(fd);
        if (current_wait_sec <= MAX_RETRY / 2) {
            print_line("Connection failed, retry after %d seconds, sit down and relax...", current_wait_sec);
            sleep(current_wait_sec);
        }
    }
    return -1;
}

void list_file() {
    connect_to_server();

    if (send(socket_fd, "l", 1, 0) != 1) {
        print_error_and_exit(ERR_FCLIENT_CONNECTION_FAILED, "Error: failed to communicate with server");
    }

    ssize_t bytes_read;
    while ((bytes_read = recv(socket_fd, buf, BUF_SIZE, 0)) > 0) {
        write(STDOUT_FILENO, buf, bytes_read);
    }
    if (bytes_read < 0) {
        print_line_stderr("Error: cannot receive data from the server");
    }
    close(socket_fd);
}

void download_file() {
    if (options.output == NULL || strlen(options.output) == 0) {    // strlen(NULL) => seg fault
        options.output = options.download;
    }

    char *lastdelim = strrchr(options.output, '/');
    if (lastdelim != NULL) {
        if (lastdelim == &options.output[strlen(options.output) - 1]) {
            print_error_and_exit(ERR_FCLIENT_FILENAME_INVALID, "Error: invalid output path name (ends with /)");
        }
        char *dirname = options.output;     // for readability; reuse options.output for dirname in makedirs()
        *lastdelim = '\0';
        if (strlen(dirname) > 0 && makedirs(dirname) != 0) {
            print_error_and_exit(ERR_FCLIENT_MKDIR_FAILED, "Error: unable to create folder: %s", strerror(errno));
        }
        *lastdelim = '/';       // recover options.output
    }

    if ((output_fd = open(options.output, O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR)) == -1) {
        print_error_and_exit(ERR_FCLIENT_FILE_NOT_AVAILABLE, "Error: unable to download to file '%s': %s", options.output, strerror(errno));
    }

    connect_to_server();

    size_t filename_size = strlen(options.download);
    if ((send(socket_fd, "d", 1, 0) != 1)
    || (send(socket_fd, (char *) &filename_size, sizeof(size_t), 0) != sizeof(size_t))
    || send(socket_fd, options.download, filename_size, 0) != filename_size) {
        err_cleanup();
        print_error_and_exit(ERR_FCLIENT_CONNECTION_FAILED, "Error: failed to communicate with server");
    }

    // first is response code
    if (recv(socket_fd, buf, 1, 0) <= 0) {
        err_cleanup();
        print_error_and_exit(ERR_FCLIENT_CONNECTION_FAILED, "Error: cannot receive response from the server");
    }
    ssize_t bytes_read;
    if (buf[0] == 's') {
        off_t filesize;
#ifdef ENABLE_PROGRESS_BAR
        off_t total_read = 0;
#endif
        if (recv(socket_fd, (char *) &filesize, sizeof(off_t), 0) <= 0) {
            err_cleanup();
            print_error_and_exit(ERR_FCLIENT_CONNECTION_FAILED, "Error: cannot receive response from the server");
        }
        print_line("downloading '%s' to '%s', size is %ld", options.download, options.output, filesize);
        while ((bytes_read = recv(socket_fd, buf, BUF_SIZE, 0)) > 0) {
            write(output_fd, buf, bytes_read);
#ifdef ENABLE_PROGRESS_BAR
            total_read += bytes_read;
            printf("\r");
            int i = 0;
            off_t display_count = total_read / filesize * 10;
            for (; i < display_count; i++) {
                printf("=");
            }
            for (; i < 10; i++) {
                printf(" ");
            }
            printf(" %ld/%ld (%.2f%%)\033[K", total_read, filesize, (float) total_read / (float) filesize * 100);
            fflush(stdout);
#endif
        }
#ifdef ENABLE_PROGRESS_BAR
        print_line("");
#endif
        if (bytes_read < 0) {
            err_cleanup();
            print_error_and_exit(ERR_FCLIENT_CONNECTION_FAILED, "Error: cannot receive data from the server");
        }
    } else if (buf[0] == 'e') {
        char *prefix = "Error: ";
        write(STDERR_FILENO, prefix, strlen(prefix));
        while ((bytes_read = recv(socket_fd, buf, BUF_SIZE, 0)) > 0) {
            write(STDERR_FILENO, buf, bytes_read);
        }
        if (bytes_read < 0) {
            err_cleanup();
            print_error_and_exit(ERR_FCLIENT_CONNECTION_FAILED, "cannot receive error message the server");
        }
        err_cleanup();
        exit(ERR_FCLIENT_INTERNAL_ERROR);
    } else {
        err_cleanup();
        print_error_and_exit(ERR_FCLIENT_INTERNAL_ERROR, "Error: invalid response from the server");
    }

    close(output_fd);
    close(socket_fd);
}

int makedirs(char *path) {
    struct stat statbuf;
    if (access(path, R_OK | W_OK) == 0) {
        if (stat(path, &statbuf) == 0 && S_ISDIR(statbuf.st_mode)) {
            return 0;
        } else {
            return -1;      // exists, but not a directory
        }
    }

    int i = 0;
    size_t len = strlen(path);
    for (; path[i] == '/'; i++);
    for (; i < len; i++) {
        if (path[i] != '/') {
            continue;
        }
        path[i] = '\0';
        if (access(path, F_OK) == -1) {
            if (mkdir(path, 0700) == -1) {
                path[i] = '/';
                return -1;
            }
        }
        path[i] = '/';
    }
    // make the last dir
    if (access(path, F_OK) == -1) {
        if (mkdir(path, 0700) == -1) {
            return -1;
        }
    }
    return 0;
}

inline void err_cleanup() {
    if (output_fd > 0) {
        close(output_fd);
        remove(options.output);
    }
}
